﻿using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Diagnostics;
using System.Text;
using Microsoft.VisualStudio.Debugger.Interop;
using UDKDevKitVSXDebugEngine;
using UDKDevKitVSXDebugEngine.Utils;
using UDKDevKitVSXDebugEngine.Definitions;

namespace UDKDevKitVSXDebugEngine
{
    public class EngineCallback
    {
        readonly IDebugEventCallback2 ad7Callback;
        readonly Engine engine;

        public EngineCallback(Engine engine, IDebugEventCallback2 ad7Callback)
        {
            if (engine == null)
            {
                Debug.Fail("ArgumentNullException - engine");
                throw new ArgumentNullException("engine");
            }

            if (ad7Callback == null)
            {
                Debug.Fail("ArgumentNullException - ad7Callback");
                throw new ArgumentNullException("ad7Callback");
            }

            this.ad7Callback = ad7Callback;
            this.engine = engine;
        }

        public void Send(IDebugEvent2 eventObject, Guid iidEvent, IDebugProgram2 program, IDebugThread2 thread)
        {
            uint attributes;
            Guid riidEvent = iidEvent;

            Core.RequireOk(eventObject.GetAttributes(out attributes));

            Debug.WriteLine("EngineCallback::Send");

            Core.RequireOk(ad7Callback.Event(engine, null, program, thread, eventObject, ref riidEvent, attributes));
        }

        public void Send(IDebugEvent2 eventObject, Guid iidEvent, IDebugThread2 thread)
        {
            Send(eventObject, iidEvent, engine, thread);
        }

        #region ISampleEngineCallback Members

        public void OnError(int hrErr)
        {
            // IDebugErrorEvent2 is used to report error messages to the user when something goes wrong in the debug engine.
            // The sample engine doesn't take advantage of this.
        }

        public void OnOutputString(string outputString)
        {
            //Debug.Assert(Worker.CurrentThreadId == engine.DebuggedProcess.PollThreadId);

            //AD7OutputDebugStringEvent eventObject = new AD7OutputDebugStringEvent(outputString);

            //Send(eventObject, AD7OutputDebugStringEvent.IID, null);
        }

        public void OnProcessExit(uint exitCode)
        {
            AD7ProgramDestroyEvent eventObject = new AD7ProgramDestroyEvent(exitCode);
            Send(eventObject, AD7ProgramDestroyEvent.IID, null);
        }

        public void OnThreadExit(uint exitCode)
        {
            //AD7ThreadDestroyEvent eventObject = new AD7ThreadDestroyEvent(exitCode);

            //Send(eventObject, AD7ThreadDestroyEvent.IID, ad7Thread);
        }

        public void OnThreadStart()
        {
            // This will get called when the entrypoint breakpoint is fired because the engine sends a thread start event
            // for the main thread of the application.

            //AD7Thread ad7Thread = new AD7Thread(engine, debuggedThread);
            //debuggedThread.Client = ad7Thread;
            
            //AD7ThreadCreateEvent eventObject = new AD7ThreadCreateEvent();
            //Send(eventObject, AD7ThreadCreateEvent.IID, ad7Thread);
        }

        public void OnBreakpoint(IList<BoundBreakpoint> clients, uint line)
        {
            IDebugBoundBreakpoint2[] boundBreakpoints;
            if (!this.ProcessBreakPoints(clients, out boundBreakpoints)
                && clients.Count > 0)
            {
                // suppress breakpoint event
                this.engine.Continue(this.engine.Thread);
                return;
            }

            AD7BreakpointEvent eventObject = new AD7BreakpointEvent(new AD7BoundBreakpointsEnum(boundBreakpoints));
            Send(eventObject, AD7BreakpointEvent.IID, this.engine.Thread);
        }

        private bool ProcessBreakPoints(IList<BoundBreakpoint> clients, out IDebugBoundBreakpoint2[] boundBreakpoints)
        {
            boundBreakpoints = new IDebugBoundBreakpoint2[clients != null ? clients.Count : 0];
            if (clients == null)
            {
                return true;
            }

            int i = 0;
            bool shouldBeRaised = false;
            foreach (BoundBreakpoint boundBreakpoint in clients)
            {
                var debugBoundBreakpoint2 = boundBreakpoint as IDebugBoundBreakpoint2;

                // increase hit count
                uint hitCount;
                if (debugBoundBreakpoint2.GetHitCount(out hitCount) == Constants.S_OK)
                {
                    debugBoundBreakpoint2.SetHitCount(hitCount + 1);
                }

                boundBreakpoints[i++] = debugBoundBreakpoint2;

                shouldBeRaised |= boundBreakpoint.ShouldBeRaised;
            }

            return shouldBeRaised;
        }

        public void OnException(uint code)
        {
            // Exception events are sent when an exception occurs in the debuggee that the debugger was not expecting.
            // The sample engine does not support these.
            throw new Exception("The method or operation is not implemented.");
        }

        public void OnStepComplete()
        {
            // Step complete is sent when a step has finished. The sample engine does not support stepping.
            throw new Exception("The method or operation is not implemented.");
        }

        public void OnAsyncBreakComplete()
        {
            // This will get called when the engine receives the breakpoint event that is created when the user
            // hits the pause button in vs.
            AD7AsyncBreakCompleteEvent eventObject = new AD7AsyncBreakCompleteEvent();
            Send(eventObject, AD7AsyncBreakCompleteEvent.IID, this.engine.Thread);
        }

        public void OnLoadComplete()
        {
            AD7LoadCompleteEvent eventObject = new AD7LoadCompleteEvent();
            Send(eventObject, AD7LoadCompleteEvent.IID, this.engine.Thread);
        }

        public void OnEntryPoint()
        {
            AD7EntryPointEvent eventObject = new AD7EntryPointEvent();
            Send(eventObject, AD7EntryPointEvent.IID, this.engine.Thread);
        }

        public void OnProgramDestroy(uint exitCode)
        {                     
            AD7ProgramDestroyEvent eventObject = new AD7ProgramDestroyEvent(exitCode);
            Send(eventObject, AD7ProgramDestroyEvent.IID, null);
        }

        // Engines notify the debugger that a breakpoint has bound through the breakpoint bound event.
        public void OnBreakpointBound(object objBoundBreakpoint)
        {
            BoundBreakpoint boundBreakpoint = (BoundBreakpoint)objBoundBreakpoint;
            IDebugPendingBreakpoint2 pendingBreakpoint;
            ((IDebugBoundBreakpoint2)boundBreakpoint).GetPendingBreakpoint(out pendingBreakpoint);

            AD7BreakpointBoundEvent eventObject = new AD7BreakpointBoundEvent((PendingBreakpoint)pendingBreakpoint, boundBreakpoint);
            Send(eventObject, AD7BreakpointBoundEvent.IID, null);
        }

        public void OnDebugExpressionEvaluationComplete(DebuggerExpression expr)
        {
            AD7DebugExpressionEvaluationCompleteEvent eventObject = new AD7DebugExpressionEvaluationCompleteEvent(expr);
            Send(eventObject, AD7DebugExpressionEvaluationCompleteEvent.IID, this.engine.Thread);
        }

        #endregion
    }
}
